<?php
if (! function_exists('is_windows')) {
    /**
     * 检测运行环境是否为windows
     * @param bool|null $mock
     * @return bool
     * @author  Codeigniter
     */
    function is_windows(?bool $mock = null): bool
    {
        static $mocked;

        if (func_num_args() === 1) {
            $mocked = $mock;
        }

        return $mocked ?? DIRECTORY_SEPARATOR === '\\';
    }
}

if ( ! function_exists('is_php'))
{
    /**
     * Determines if the current version of PHP is equal to or greater than the supplied value
     *
     * @param string|int|float $version
     * @return	bool	TRUE if the current version is $version or higher
     */
    function is_php($version):bool
    {
        $version = (string) $version;
        return version_compare(PHP_VERSION, $version, '>=');
    }
}
if (extension_loaded('mbstring'))
{
    define('MB_ENABLED', TRUE);
    $charset_tmp = ini_get('default_charset');
    if(empty($charset)){
        global $conf;
        if(!empty($conf))
            $charset_tmp = $conf['db']['master']['charset'] ?? null;
        else{ //如果没有初始化：
            $_cfgs = include APP_PATH.'conf/conf.php';
            $charset_tmp = $_cfgs['db']['master']['charset'] ?? null;
            unset($_cfgs);
        }
        if(empty($charset_tmp) || $charset_tmp == 'utf8') $charset_tmp ='UTF-8';
        @ini_set('default_charset', $charset_tmp);
        if (is_php('5.6')) @ini_set('php.internal_encoding', $charset_tmp);
    }
    if(empty(@ini_get('mbstring.internal_encoding'))){
        // mbstring.internal_encoding is deprecated starting with PHP 5.6
        // and it's usage triggers E_DEPRECATED messages.
        @ini_set('mbstring.internal_encoding', $charset_tmp);
        // This is required for mb_convert_encoding() to strip invalid characters.
        // That's utilized by CI_Utf8, but it's also done for consistency with iconv.
        mb_substitute_character('none');
    }
    unset($charset_tmp);
}
else
{
    define('MB_ENABLED', FALSE);
}

// There's an ICONV_IMPL constant, but the PHP manual says that using
// iconv's predefined constants is "strongly discouraged".
if (extension_loaded('iconv'))
{
    define('ICONV_ENABLED', TRUE);
    // iconv.internal_encoding is deprecated starting with PHP 5.6
    // and it's usage triggers E_DEPRECATED messages.
    @ini_set('iconv.internal_encoding', ini_get('default_charset') ?: 'utf-8');
}
else
{
    define('ICONV_ENABLED', FALSE);
}
if ( ! function_exists('mb_strlen'))
{
    /**
     * mb_strlen()
     *
     * WARNING: This function WILL fall-back to strlen()
     * if iconv is not available!
     *
     * @link	http://php.net/mb_strlen
     * @param string $str
     * @param string|null $encoding
     * @return	int
     */
    function mb_strlen(string $str, ?string $encoding = null):int
    {
        if(empty($encoding)){
            $encoding = @ini_get('default_charset') ?? 'UTF-8';
        }
        if (ICONV_ENABLED === TRUE)
        {
            return iconv_strlen($str, $encoding);
        }
        return strlen($str);
    }
}
if ( ! function_exists('mb_strpos'))
{
    /**
     * mb_strpos()
     *
     * WARNING: This function WILL fall-back to strpos()
     * if iconv is not available!
     *
     * @link	http://php.net/mb_strpos
     * @param	string	$haystack
     * @param	string	$needle
     * @param	int	$offset
     * @param	string	$encoding
     * @return	mixed
     */
    function mb_strpos($haystack, $needle, $offset = 0, $encoding = NULL)
    {
        if (ICONV_ENABLED === TRUE)
        {
            return iconv_strpos($haystack, $needle, $offset, isset($encoding) ? $encoding : ini_get('default_charset'));
        }
        return strpos($haystack, $needle, $offset);
    }
}

if ( ! function_exists('mb_substr'))
{
    /**
     * mb_substr()
     *
     * WARNING: This function WILL fall-back to substr()
     * if iconv is not available.
     *
     * @link	http://php.net/mb_substr
     * @param	string	$str
     * @param	int	$start
     * @param	int 	$length
     * @param	string	$encoding
     * @return	string
     */
    function mb_substr($str, $start, $length = NULL, $encoding = NULL):string
    {
        if (ICONV_ENABLED === TRUE)
        {
            isset($encoding) OR $encoding = ini_get('default_charset');
            return iconv_substr(
                $str,
                $start,
                isset($length) ? $length : iconv_strlen($str, $encoding), // NULL doesn't work
                $encoding
            );
        }
        return isset($length)
            ? substr($str, $start, $length)
            : substr($str, $start);
    }
}

if (! function_exists('str_ends_with')) { //PHP8+才内置str_ends_with()
    function str_ends_with($haystack, $needle) {
        $length = strlen($needle);
        if ($length == 0) {
            return true;
        }
        return substr($haystack, -$length) === $needle;
    }
}
if (! function_exists('str_contains')) {//PHP8+才内置str_contains()
    function str_contains($haystack, $needle) {
        return strpos($haystack, $needle) !== false;
    }
}


class LabelReplacer
{
    public $default_val;
    public $vars;
    public $replace_pos=0;

    public function &DoReplace(array &$matches)
    {
        $ret = '';
        $mcs = count($matches);
        $rpos = $this->replace_pos;
        if($rpos < 0) $rpos = $mcs + $rpos;
        $k = $matches[$rpos];
        if ($this->vars != null && isset($this->vars[$k])) $matches[$rpos] = $this->vars[$k];
        else {
            if ($this->default_val !== null) $matches[$rpos] = $this->default_val;
        }
        if (!empty($matches[$rpos])) {
            unset($matches[0]);
            $ret = implode('', $matches);
        }
        return $ret;
    }
    public function &ReplaceStrVar(array &$matches){
        $ret = null;
        $va = &$matches[1];
        if(!empty($va)){
            if(isset($this->vars[$va]))
                $ret =  $this->vars[$va];
            else{
                if(defined($va))
                    $ret =  constant($va);
            }
        }
        if($ret === null){
            $ret =  ($this->default_val===null?$matches[0]:$this->default_val);
        }
        return $ret;
    }

    public function &ReplaceHeredoc(array &$matches){
        static $rx = 0;
        if(!isset($this->vars)) $this->vars = array();
        $keyx =  'H_e_r_e_D_O_C_'.$rx++;
        $this->vars[$keyx] = &$matches[2];
        $ret = "{$matches[1]}'{$keyx}';";
        return $ret;
    }

}

/**
 * 替换字符串里的{var}
 * @param string|array $text
 * @param array $arr
 * @param mixed $default_val 当变量标记不存在时的替换值,null为保留原样标记
 * @param string $var_prefix 变量标记的前缀，如#，则变量标签为：{#var}
 * @return string|string[]
 */
function &filltextvar($text,array $arr = array(),$default_val=null,string $var_prefix='')
{
    if (strlen($text) < 3 + strlen($var_prefix))
        return $text;
    $lr = new LabelReplacer();
    $lr->default_val = '';
    $lr->replace_pos = -1;
    $lr->vars = &$arr;
    $ret = preg_replace_callback('/\{'.(empty($var_prefix) ? '' : preg_quote($var_prefix,'/')).'([^{}]*?)\b(\w+)}/u',array($lr,'DoReplace'),$text);
    return $ret;
}


/**
 * Unix to "Human"
 *
 * Formats Unix timestamp to the following prototype: 2006-08-21 11:35 PM
 *
 * @access	public
 * @param	integer Unix timestamp
 * @param	bool	whether to show seconds
 * @param	string	format: us or euro
 * @return	string
 * @author ionize
 */
if ( ! function_exists('unix_to_human'))
{
    function unix_to_human($time = '', $seconds = FALSE, $fmt = 'us')
    {
        $r  = date('Y', $time).'-'.date('m', $time).'-'.date('d', $time).' ';

        if ($fmt == 'us')
        {
            $r .= date('h', $time).':'.date('i', $time);
        }
        else
        {
            $r .= date('H', $time).':'.date('i', $time);
        }

        if ($seconds)
        {
            $r .= ':'.date('s', $time);
        }

        if ($fmt == 'us')
        {
            $r .= ' '.date('A', $time);
        }

        return $r;
    }
}


if ( ! function_exists('human_to_unix'))
{
    /**
     * Convert "human" date to GMT
     *
     * Reverses the above process
     *
     * @access	public
     * @param	string	format: us or euro
     * @return	integer
     * @author ionize
     */
    function human_to_unix($datestr = ''):int
    {
        if ($datestr == '')
        {
            return FALSE;
        }

        $datestr = trim($datestr);
        $datestr = preg_replace("/\040+/", ' ', $datestr);

        if ( ! preg_match('/^[0-9]{2,4}-[0-9]{1,2}-[0-9]{1,2}\s[0-9]{1,2}:[0-9]{1,2}(?::[0-9]{1,2})?(?:\s[AP]M)?$/i', $datestr))
        {
            return FALSE;
        }

        $split = explode(' ', $datestr);

        $ex = explode("-", $split['0']);

        $year  = (strlen($ex['0']) == 2) ? '20'.$ex['0'] : $ex['0'];
        $month = (strlen($ex['1']) == 1) ? '0'.$ex['1']  : $ex['1'];
        $day   = (strlen($ex['2']) == 1) ? '0'.$ex['2']  : $ex['2'];

        $ex = explode(":", $split['1']);

        $hour = (strlen($ex['0']) == 1) ? '0'.$ex['0'] : $ex['0'];
        $min  = (strlen($ex['1']) == 1) ? '0'.$ex['1'] : $ex['1'];

        if (isset($ex['2']) && preg_match('/[0-9]{1,2}/', $ex['2']))
        {
            $sec  = (strlen($ex['2']) == 1) ? '0'.$ex['2'] : $ex['2'];
        }
        else
        {
            // Unless specified, seconds get set to zero.
            $sec = '00';
        }

        if (isset($split['2']))
        {
            $ampm = strtolower($split['2']);

            if (substr($ampm, 0, 1) == 'p' AND $hour < 12)
                $hour = $hour + 12;

            if (substr($ampm, 0, 1) == 'a' AND $hour == 12)
                $hour =  '00';

            if (strlen($hour) == 1)
                $hour = '0'.$hour;
        }

        return mktime($hour, $min, $sec, $month, $day, $year);
    }
}
if (!function_exists('timezones')) {
    /**
     * Timezones
     *
     * Returns an array of timezones. This is a helper function
     * for various other ones in this library
     *
     * @param string    timezone
     * @return    string|array
     */
    function timezones($tz = '')
    {
        // Note: Don't change the order of these even though
        // some items appear to be in the wrong order

        $zones = array(
            'UM12'		=> -12,
            'UM11'		=> -11,
            'UM10'		=> -10,
            'UM95'		=> -9.5,
            'UM9'		=> -9,
            'UM8'		=> -8,
            'UM7'		=> -7,
            'UM6'		=> -6,
            'UM5'		=> -5,
            'UM45'		=> -4.5,
            'UM4'		=> -4,
            'UM35'		=> -3.5,
            'UM3'		=> -3,
            'UM2'		=> -2,
            'UM1'		=> -1,
            'UTC'		=> 0,
            'UP1'		=> +1,
            'UP2'		=> +2,
            'UP3'		=> +3,
            'UP35'		=> +3.5,
            'UP4'		=> +4,
            'UP45'		=> +4.5,
            'UP5'		=> +5,
            'UP55'		=> +5.5,
            'UP575'		=> +5.75,
            'UP6'		=> +6,
            'UP65'		=> +6.5,
            'UP7'		=> +7,
            'UP8'		=> +8,
            'UP875'		=> +8.75,
            'UP9'		=> +9,
            'UP95'		=> +9.5,
            'UP10'		=> +10,
            'UP105'		=> +10.5,
            'UP11'		=> +11,
            'UP115'		=> +11.5,
            'UP12'		=> +12,
            'UP1275'	=> +12.75,
            'UP13'		=> +13,
            'UP14'		=> +14
        );

        if ($tz == '')
        {
            return $zones;
        }

        if ($tz == 'GMT')
            $tz = 'UTC';

        return ( ! isset($zones[$tz])) ? 0 : $zones[$tz];
    }
}
if (!function_exists('now')) {
    /**
     * Get "now" time
     *
     * Returns time() based on the timezone parameter or on the
     * "time_reference" setting
     *
     * @param string
     * @return    int
     */
    function now($timezone = NULL)
    {
        if (empty($timezone)) {
            global $conf;
            $timezone = $conf['timezone'] ?: ini_get('date.timezone');
        }

        if ($timezone === 'local' OR $timezone === date_default_timezone_get()) {
            return time();
        }

        $datetime = new DateTime('now', new DateTimeZone($timezone));
        sscanf($datetime->format('j-n-Y G:i:s'), '%d-%d-%d %d:%d:%d', $day, $month, $year, $hour, $minute, $second);

        return mktime($hour, $minute, $second, $month, $day, $year);
    }
}
if (!function_exists('timespan')) {
    /**
     * Timespan
     *
     * Returns a span of seconds in this format:
     *    10 days 14 hours 36 minutes 47 seconds
     * 这个函数最常见的用途是，显示从过去某个时间点到当前时间经过了多少时间。
     * 将一个 UNIX 时间戳转换为以下这种格式:1 Year, 10 Months, 2 Weeks, 5 Days, 10 Hours, 16 Minutes
     * @param int    a number of seconds
     * @param int    Unix timestamp
     * @param int    a number of display units
     * @return    string
     */
    function timespan($seconds = 1, $time = '', $units = 7)
    {
        is_numeric($seconds) OR $seconds = 1;
        is_numeric($time) OR $time = time();
        is_numeric($units) OR $units = 7;

        $seconds = ($time <= $seconds) ? 1 : $time - $seconds;

        $str = array();
        $years = floor($seconds / 31557600);

        if ($years > 0) {
            $str[] = $years . ' 年';
        }

        $seconds -= $years * 31557600;
        $months = floor($seconds / 2629743);

        if (count($str) < $units && ($years > 0 OR $months > 0)) {
            if ($months > 0) {
                $str[] = $months . ' 月';
            }

            $seconds -= $months * 2629743;
        }

        $weeks = floor($seconds / 604800);

        if (count($str) < $units && ($years > 0 OR $months > 0 OR $weeks > 0)) {
            if ($weeks > 0) {
                $str[] = $weeks . ' 周';
            }

            $seconds -= $weeks * 604800;
        }

        $days = floor($seconds / 86400);

        if (count($str) < $units && ($months > 0 OR $weeks > 0 OR $days > 0)) {
            if ($days > 0) {
                $str[] = $days . ' 天';
            }

            $seconds -= $days * 86400;
        }

        $hours = floor($seconds / 3600);

        if (count($str) < $units && ($days > 0 OR $hours > 0)) {
            if ($hours > 0) {
                $str[] = $hours . ' 时';
            }

            $seconds -= $hours * 3600;
        }

        $minutes = floor($seconds / 60);

        if (count($str) < $units && ($days > 0 OR $hours > 0 OR $minutes > 0)) {
            if ($minutes > 0) {
                $str[] = $minutes . ' 分';
            }

            $seconds -= $minutes * 60;
        }

        if (count($str) === 0) {
            $str[] = $seconds . ' 秒';
        }

        return implode(', ', $str);
    }
}

if (!function_exists('local_to_gmt')) {
    /**
     * 将时间转换为 GMT 时间。
     *
     * @param int    Unix timestamp
     * @return    int
     */
    function local_to_gmt($time = '')
    {
        if ($time === '') {
            $time = time();
        }

        return mktime(
            gmdate('G', $time),
            gmdate('i', $time),
            gmdate('s', $time),
            gmdate('n', $time),
            gmdate('j', $time),
            gmdate('Y', $time)
        );
    }
}

if (!function_exists('gmt_to_local')) {
    /**
     * 根据指定的时区和 DST （夏令时，Daylight Saving Time） 将 GMT 时间转换为本地时间。
     *
     * @param int    Unix timestamp
     * @param string    timezone
     * @param bool    whether DST is active
     * @return    int
     */
    function gmt_to_local($time = '', $timezone = 'UTC', $dst = FALSE):int
    {
        if ($time == '')
        {
            return now();
        }

        $time += timezones($timezone) * 3600;

        if ($dst == TRUE)
        {
            $time += 3600;
        }

        return $time;
    }
}
if (!function_exists('msectime')) {
    /**
     * 当前毫秒时间戳
     * @param string $sprintf_format 小数的格式符,默认为%.0f
     * @return float
     */
    function msectime($sprintf_format = '%.0f')
    {
        list($msec, $sec) = explode(' ', microtime());
        return (float)sprintf($sprintf_format, (floatval($msec) + floatval($sec)) * 1000);
    }
}

if (!function_exists('date_range')) {
    /**
     * Date range
     *返回某一段时间的日期列表。
     * Returns a list of dates within a specified period.
     *
     * @param int    unix_start    UNIX timestamp of period start date
     * @param int    unix_end|days    UNIX timestamp of period end date
     *                    or interval in days.
     * @param mixed    is_unix        Specifies whether the second parameter
     *                    is a UNIX timestamp or a day interval
     *                     - TRUE or 'unix' for a timestamp
     *                     - FALSE or 'days' for an interval
     * @param string  date_format    Output date format, same as in date()
     * @return    array|false
     */
    function date_range($unix_start = '', $mixed = '', $is_unix = TRUE, $format = 'Y-m-d')
    {
        if ($unix_start == '' OR $mixed == '' OR $format == '') {
            return FALSE;
        }

        $is_unix = !(!$is_unix OR $is_unix === 'days');

        // Validate input and try strtotime() on invalid timestamps/intervals, just in case
        if ((!ctype_digit((string)$unix_start) && ($unix_start = @strtotime($unix_start)) === FALSE)
            OR (!ctype_digit((string)$mixed) && ($is_unix === FALSE OR ($mixed = @strtotime($mixed)) === FALSE))
            OR ($is_unix === TRUE && $mixed < $unix_start)) {
            return FALSE;
        }

        if ($is_unix && ($unix_start == $mixed OR date($format, $unix_start) === date($format, $mixed))) {
            return array(date($format, $unix_start));
        }

        $range = array();

        $from = new DateTime();
        $from->setTimestamp($unix_start);

        if ($is_unix) {
            $arg = new DateTime();
            $arg->setTimestamp($mixed);
        } else {
            $arg = (int)$mixed;
        }

        $period = new DatePeriod($from, new DateInterval('P1D'), $arg);
        foreach ($period as $date) {
            $range[] = $date->format($format);
        }

        /* If a period end date was passed to the DatePeriod constructor, it might not
         * be in our results. Not sure if this is a bug or it's just possible because
         * the end date might actually be less than 24 hours away from the previously
         * generated DateTime object, but either way - we have to append it manually.
         */
        if (!is_int($arg) && $range[count($range) - 1] !== $arg->format($format)) {
            $range[] = $arg->format($format);
        }

        return $range;
    }
}

/**
 * 去掉HTML标签
 * @param string $s
 * @param boolean $allowbr 是否保留<br/>,默认为是
 * @return string
 */
function strip_html(string $s, $allowbr = true, $allowable_tags = null)
{
    if (!empty($s)) {
        $s = str_ireplace(array('<br>', '<br />', '<br/>', '</p>', '</tr>', '</div>', '</li>', '</dd>' . '</dt>'), "\r\n", $s);
        $s = str_ireplace(array('&nbsp;'), " ", $s);
        $s = strip_tags($s, $allowable_tags);
        $s = preg_replace('#[\r\n]+#', "\n", $s);
        if ($allowbr) $s = str_replace("\n", '<br/>', $s);
    }
    return $s;
}
/**
 * 是否为本地/同族域名host
 * @param string $path 要检测的网址
 * @param array|string|null $KindredClass 同族域名表,用*表示泛子域名
 * @return bool
 */
function IsKindredHost(string $path, $KindredClass = null)
{
    if (empty($path)) return false;
    $sps = 0;
    $hp1 = stripos($path, 'http://');
    if ($hp1 !== 0) {
        $hp1 = stripos($path, 'https://');
        if ($hp1 === 0) {
            $sps = strlen('https://');
        } else
            return true;
    } else
        $sps = strlen('http://');
    $spe = strpos($path, '/', $sps + 3);
    if ($spe === false) $spe = strlen($path);
    $srchost = strtolower(substr($path, $sps, $spe - $sps));
    if (!empty($path) && is_string($KindredClass)) {
        $KindredClass = preg_split('#[\s,;，；|/]+#is', $KindredClass, -1, PREG_SPLIT_NO_EMPTY);
    }
    if (!empty($KindredClass)) {
        foreach ($KindredClass as &$kc) {
            $kc = strtolower($kc);
            if (stripos($kc, 'www.') === 0)
                $kc = substr($kc, 4);
        }
    }
    $hostparts = explode('.', $srchost);
    if ($hostparts[0] == 'www') array_shift($hostparts);
    $srchost = implode('.',$hostparts);
    $my_host = strtolower($_SERVER['HTTP_HOST']);
    if (stripos($my_host, 'www.') === 0)
        $my_host = substr($my_host, 4);
    if ($srchost == $my_host)
        return true;
    if (empty($KindredClass))
        return false;
    $srchost2 = '*.' . implode('.',$hostparts);
    $srchost3 = null;
    if (count($hostparts) > 2) {
        array_shift($hostparts);
        $srchost3 = '*.' . implode('.',$hostparts);
    }
    if (in_array($srchost, $KindredClass) || in_array($srchost2, $KindredClass))
        return true;
    if (!empty($srchost3) && in_array($srchost3, $KindredClass))
        return true;
    return true;
}
/**
 * Convert Reserved XML characters to Entities
 * 将输入字符串中的下列 XML 保留字符转换为实体（Entity）：<br/>
 * 和号：&<br/>
 * 小于号和大于号：< ><br/>
 * 单引号和双引号：' "<br/>
 * 减号：-<br/>
 * 如果 & 符号是作为实体编号的一部分，例如： &#123; ，该函数将不予处理。<br/>
 * @link  https://codeigniter.org.cn/user_guide/helpers/xml_helper.html?highlight=xml_convert#xml_convert
 * @param string $str 要转换的文本字符串
 * @param bool $protectAll 是否保护看起来像潜在实体的所有内容,而不仅仅是编号的实体,例如 &foo;
 * @return    string XML 转换后的字符串
 * @uses 示例：<br/>
 * $string = '<p>Here is a paragraph & an entity (&#123;).</p>';<br/>
 * $string = xml_convert($string);<br/>
 * echo $string;<br/>
 * 输出结果:&lt;p&gt;Here is a paragraph &amp; an entity (&#123;).&lt;/p&gt;
 * @author Codeigniter
 */
function xml_convert(string $str, bool $protectAll = false): string
{
    $temp = '__TEMP_AMPERSANDS__';

    // Replace entities to temporary markers so that
    // ampersands won't get messed up
    $str = preg_replace('/&#(\d+);/', $temp . '\\1;', $str);

    if ($protectAll === true) {
        $str = preg_replace('/&(\w+);/', $temp . '\\1;', $str);
    }

    $original = [
        '&',
        '<',
        '>',
        '"',
        "'",
        '-',
    ];

    $replacement = [
        '&amp;',
        '&lt;',
        '&gt;',
        '&quot;',
        '&apos;',
        '&#45;',
    ];

    $str = str_replace($original, $replacement, $str);

    // Decode the temp markers back to entities
    $str = preg_replace('/' . $temp . '(\d+);/', '&#\\1;', $str);

    if ($protectAll === true) {
        return preg_replace('/' . $temp . '(\w+);/', '&\\1;', $str);
    }

    return $str;
}
/**
 * 相对路径转换为绝对路径
 * @param string $in_rel
 * @param string $out_abs
 * @return int
 */
function RelpathToAbspath(string $in_rel, string &$out_abs)
{
    $i_rtn = 0;  // return value
    $ss_rel = "";  // for relative path build
    $st_fpos = 0;    // front separator index
    $sv_path = [];   // divide path to array

    if (file_exists($in_rel)) {
        $out_abs = realpath($in_rel);
        $out_abs = is_dir($out_abs) ? rtrim($out_abs, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR : $out_abs;
        return $i_rtn;
    }
    $st_pos = strpos($in_rel, DIRECTORY_SEPARATOR);
    $npos = 0;
    while ($npos != $st_pos) {
        if ($st_pos != 0) {
            array_push($sv_path, substr($in_rel, $st_fpos, $st_pos - $st_fpos));
        }
// next...
        $st_fpos = $st_pos;   // set current pos to last pos
        $st_pos++;            // from next index
        $st_pos = strpos($in_rel, DIRECTORY_SEPARATOR, $st_pos);  // next separator index
    } // while ( $npos != $st_pos )
// final separator
    array_push($sv_path, substr($in_rel, $st_fpos));

    $lpc = 0;    // loop count
    $i_max = count($sv_path);
    while ($lpc < $i_max && 0 === $i_rtn) {
        $ss_rel .= $sv_path[$lpc];
// relative path => relative path
        $c_abs = realpath($ss_rel);
        if ($c_abs === false) {
            $i_rtn = -1;
        } else {
            $ss_rel = $c_abs;
            $i_rtn = 0;
        }
        $lpc++;
    } // while (count($sv_path)>0)

// normal ending
    if (0 === $i_rtn) {
        $out_abs = $ss_rel;  // set converted path
        $out_abs = is_dir($out_abs) ? rtrim($out_abs, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR : $out_abs;
    }
    return $i_rtn;
}
/**
 * 去除引号内的内容
 * @param string $str 待处理的字符串
 * @param string $repChar 引号内的替换字符,默认为*
 * @param string|NULL $exclude_quote_boxchr 以此2字符包裹的引号不会当成引号字符串的一部分(引号内的内容除外),用于对sql语句的分析,一般为``,如字段名中含有',例如 `field'X`
 * @param array|NULL $QuoteList 内容分节输出数组,默认为空,不输出.引号内容节点(包含头尾的引号)为array('***'=>'原内容')
 * @param string $cslashes 字符串中的转义符,默认为\,sql中可能为'
 * @param string $prefix 要附加的字符串前缀,默认为空
 * @param string $suffix 要附加的字符串后缀,默认为空
 * @return string
 * @uses 示例用法：
 * <br/>$str = 'abort on : "为什么官僚们把文\\\\\"革解释为\'食年浩截\'?",this is real?! -- \'http://www.szhgh.com/Article/opinion/zatan/201401/41886.html  http://www.szhgh.com/m/show.php?classid=49&id=65714\'';
 * <br/>$val = filtrate_quote_centent($str);
 * <br/>echo 'filtrate_quote_centent($str):'.$val.PHP_EOL;
 * <br/>$sql = 'SELECT id,`name\'x`,`name\'m` FROM worker WHERE addr LIKE "%黑龙江\\%" AND age > getage(\'成年年龄\')';
 * <br/>$quote_list = array();
 * <br/>$val = filtrate_quote_centent($sql,'*','``',$quote_list);
 * <br/>print_r($quote_list);
 * <br/>echo $val;
 */
function filtrate_quote_centent(string $str, string $repChar='*', string $exclude_quote_boxchr=NULL, array &$QuoteList=NULL, string $cslashes='\\', string $prefix='', string $suffix=''):string
{
    if (empty($str))return $str;
    $strlen = mb_strlen($str);
    if ($strlen < 1)return $str;
    $c1 = NULL;
    $b = -1;
    $xgcount = 0;
    $exclude_quote_arae= FALSE;//进入引号排除区域
    $qlen = mb_strlen($prefix);
    $hlen = mb_strlen($suffix);
    $ret = $prefix . $str . $suffix;
    $splitexp='/./u';
    if(preg_match_all($splitexp,$ret,$matches)== $strlen + $qlen + $hlen)
    {
        $re = &$matches[0];
        $otherchr = array();//非引号内的字符数组
        for ($i = $qlen; $i < $strlen + $qlen; $i++)
        {
            if ($c1 === NULL && $exclude_quote_boxchr!== NULL && $re[$i]== $exclude_quote_boxchr[intval($exclude_quote_arae)])
            {
                $exclude_quote_arae= !$exclude_quote_arae;//进入|退出引号排除区域
            }
            if ( !$exclude_quote_arae && ($re[$i]== '\'' || $re[$i]== '"'))
            {
                if ($c1 === NULL)
                {
                    $b = $i;
                    $c1 = $re[$i];
                    if(is_array($QuoteList)&& !empty($otherchr))
                    {
                        $QuoteList[]= implode('',$otherchr);
                        $otherchr = array();
                    }
                }
                else
                {
                    if ($re[$i]== $c1 && $xgcount % 2 == 0)
                    {
                        $txt = array_slice($re,$b,$i-$b + 1);
                        $repr = $re[$b];
                        for ($j = $b + 1; $j < $i; $j++)
                        {
                            $re[$j] =$repChar;
                            $repr .= $repChar;
                        }
                        $repr .= $re[$i];
                        $c1 = NULL;
                        if(is_array($QuoteList)&& !empty($txt))
                        {
                            $QuoteList[]= array($repr=>implode('',$txt));
                        }
                    }
                }
            }
            else
            {
                if($c1 === NULL &&is_array($QuoteList))
                {
                    $otherchr[]= $re[$i];
                }
            }
            if($c1 !== NULL)
            {
                if ($re[$i]== $cslashes)
                    $xgcount++;
                else
                    $xgcount = 0;
            }
        }
        $ret = implode('',$re);
        if(is_array($QuoteList)&& !empty($otherchr))//结果链的最后一部分
        {
            $QuoteList[]= implode('',$otherchr);
        }
    }
    return $ret;
}

/**
 * 以正则式分隔字符串,会识别引号中的内容（即忽略引号中的分隔符）
 * @param string  $str 待分隔的字符串
 * @param string  $regex 分隔符的正则式
 * @param string  $repChar 引号中内容的替换字符,注意:不能与分隔字符相同
 * @param string|NULL $exclude_quote_boxchr 以此2字符包裹的引号不会当成引号字符串的一部分(引号内的内容除外),用于对sql语句的分析,一般为``,如字段名中含有',例如 `field'X`
 * @return array|string
 */
function split_discern_quote(string $str,string $regex,string $repChar='*', string $exclude_quote_boxchr=NULL)
{
    if(empty($str)|| empty($regex))
        return $str;
    $quote_list = array();
    $stx = filtrate_quote_centent($str,$repChar,$exclude_quote_boxchr,$quote_list);
    $dels = 0;
    foreach ($quote_list as $k => &$item)
    {
        if(is_string($item))
        {
            $item = preg_split($regex,$item);
        }
        else
        {
            $item = current($item);
            if($k > 0)
            {
                if(is_string($quote_list[$k - 1]))//合并连续的引号内容
                {
                    $quote_list[$k - 1].= $item;
                    unset($quote_list[$k]);
                    $dels++;
                }
            }
        }
    }
    if($dels > 0)  $quote_list = array_values($quote_list);//重排数字索引
    foreach ($quote_list as $k => &$item)//合并引号串
    {
        if(is_string($item))
        {
            if($k > 0)//抽出前一组的最后一个元素:
            {
                $stack = array_pop($quote_list[$k-1]);
                if(!empty($stack))
                    $item = $stack.$item;
            }
            if(isset($quote_list[$k + 1])&& is_array($quote_list[$k + 1]))//后一组:
            {
                $stack = array_shift($quote_list[$k + 1]);
                if(!empty($stack))$item .= $stack;
            }
        }
    }
    //把空的数组删除:
    $items = array();
    foreach ($quote_list as $k => &$item)
    {
        if(is_array($item))
        {
            foreach ($item as &$it)
            {
                $items[]= &$it;
            }
        }
        else
            $items[]= &$item;
    }
    return $items;
}

/**
 * 以字符串的第一个字符或者最后一个字符分隔成数组,如果首尾字符不是字母数字和_,则用$default_delimiter=';',可以根据需要跳过中间的()[]{}""''
 * @param string &$str 要分隔的字符串,引用传入
 * @param string $default_delimiter 缺省的分隔字符
 * @param int $jump_scope 要跳过的{1}"2"(4)[8]'16'范围,用位示法,默认或者null为跳过{}""
 * @param int|null $limit 数量参数
 * @param string|null $box_pair 额外的包装符,会解包,去掉包装符,有转义符时必须在[2]上标记
 * @return array
 */
function &explode_chr_head_tail(string &$str, $default_delimiter = ';', $jump_scope = 3, $limit = null, $box_pair = null)
{
    $ret = array();
    if (!empty($str)) {
        $strlength = strlen($str);
        $testchr_len = 1;
        if ($limit > 1 || $limit === null) {
            $testchr = $str[0];
            $tord = ord($testchr);
            if(function_exists('mb_strlen')){
                $testchr = mb_substr($str,0,1);
                $testchr_len = strlen($testchr);
            }
            $delimiter = 0;
            if ($testchr_len == 1 && !(($tord > 47 && $tord < 58) || ($tord > 64 && $tord < 91) || ($tord > 96 && $tord < 123) || $tord == 95))
                $delimiter = $testchr;
            else {
                $testchr = $str[$strlength - 1];
                $tord = ord($testchr);
                if(function_exists('mb_strlen')){
                    $testchr = mb_substr($str,-1);
                    $testchr_len = strlen($testchr);
                }
                if ($testchr_len == 1 && !(($tord > 47 && $tord < 58) || ($tord > 64 && $tord < 91) || ($tord > 96 && $tord < 123) || $tord == 95))
                    $delimiter = $testchr;
            }
            if($jump_scope === null) $jump_scope = 3;
            if (empty($delimiter)) $delimiter = (empty($default_delimiter) ? ';' : $default_delimiter);
            if ($jump_scope > 0) {
                $start = $cursor = 0;
                $scope_pair = array(
                    1 => '{}',
                    2 => '""\\',
                    4 => '()',
                    8 => '[]',
                    16 => '\'\'\\'
                );//[0]:开始字符,[1]:结束字符,[2]:转义字符
                $scope_find = array();
                if (!empty($box_pair)) {
                    $boxid = array_search($box_pair, $scope_pair);
                    if ($boxid !== false) {
                        if (($jump_scope & $boxid) == 0)
                            $jump_scope |= $boxid;
                    } else {
                        end($scope_pair);
                        $scope_pair[key($scope_pair) * 2] = $box_pair;//添加
                    }
                }

                do {
                    $to_continue = array();
                    $p = strpos($str, $delimiter, $cursor);
                    if ($p !== false) {
                        if ($p > $start) {//跳过连续的分隔符
                            foreach ($scope_pair as $scopeKey => $scope2) {
                                if (!isset($scope_find[$scopeKey]))
                                    $scope_find[$scopeKey] = array(-1, false);
                                if (($jump_scope & $scopeKey) > 0 && $scope_find[$scopeKey][0] !== false) {
                                    $pp = &$scope_find[$scopeKey][0];
                                    $ppe = &$scope_find[$scopeKey][1];
                                    if ($pp < $cursor) {
                                        $cursorX = $cursor;
                                        do {
                                            $pp = strpos($str, $scope2[0], $cursorX);
                                            $cursorX = get_array_boundary($to_continue, $pp);
                                        } while ($cursorX != $pp);

                                        if ($pp !== false) {
                                            $cursor2 = $pp + 1;
                                            while ($cursor2 < $strlength) {
                                                $ppe = strpos($str, $scope2[1], $cursor2);
                                                if ($ppe !== false && count($scope2[0]) > 2) {
                                                    $cslashes = 0;//转义符的个数
                                                    $backp = $ppe - 1;
                                                    while ($backp > -1) {
                                                        if ($str[$backp] == $scope2[2]) {
                                                            $backp--;
                                                            $cslashes++;
                                                        } else
                                                            break;
                                                    }
                                                    if ($cslashes % 2 == 1) {
                                                        $cursor2 = $ppe + 1;
                                                    } else
                                                        break;
                                                }
                                            }
                                        } else
                                            $ppe = false;
                                    }
                                    if ($ppe !== false && $pp < $p && $ppe > $p) {
                                        $to_continue[$pp] = $ppe;
                                    }
                                    unset($pp, $ppe);
                                }
                            }
                            //跳过范围内的$delimiter
                            if (!empty($to_continue)) {
                                //把在其它范围内的删除掉,只保留独立的范围:
                                $min_scope = array_keys($to_continue);
                                $scope_len = count($min_scope);
                                for ($i = 0; $i < $scope_len; $i++) {
                                    for ($j = 0; $j < $scope_len; $j++) {
                                        if ($i == $j) continue;
                                        if ($min_scope[$i] > $min_scope[$j] && $min_scope[$i] < $to_continue[$min_scope[$j]]) {
                                            unset($to_continue[$min_scope[$j]]);
                                            $min_scope = array_keys($to_continue);
                                            $scope_len = count($min_scope);
                                            $i--;
                                            break;
                                        }
                                    }
                                }
                                $cursor = max($to_continue) + 1;
                                continue;
                            }
                            $str_mid = substr($str, $start, $p - $start);
                            //可能要去掉包装符
                            if (empty($box_pair) || $str_mid[0] != $box_pair[0] || $str_mid[strlen($str_mid) - 1] != $box_pair[1])
                                $ret[] = trim($str_mid);
                            else {
                                $ret[] = trim(substr($str_mid, 1, -1));
                            }

                            if ($limit !== null) {
                                $limit--;
                                if ($limit < 2) {
                                    $start = $cursor = $p + 1;
                                    break;
                                }
                            }
                        }
                        $start = $cursor = $p + 1;
                    }
                } while ($p !== false);
                if ($start < $strlength) {//最后一段:
                    $ret[] = substr($str, $start);
                }
            } else {
                $retX = explode($delimiter, $str, $limit);
                foreach ($retX as &$rex) {//去除空:
                    if (!empty($rex)) $ret[] = &$rex;
                }
                unset($retX);
            }
        } else {
            if ($limit == 1) $ret[] = &$str;
        }
    }
    return $ret;
}


/**
 * The following function was derived from code of Symfony (v6.2.7 - 2023-02-28)
 * https://github.com/symfony/symfony/blob/80cac46a31d4561804c17d101591a4f59e6db3a2/src/Symfony/Component/String/ByteString.php#L45
 * Code subject to the MIT license (https://github.com/symfony/symfony/blob/v6.2.7/LICENSE).
 * Copyright (c) 2004-present Fabien Potencier
 *
 * The following method was derived from code of the Hack Standard Library (v4.40 - 2020-05-03)
 * https://github.com/hhvm/hsl/blob/80a42c02f036f72a42f0415e80d6b847f4bf62d5/src/random/private.php#L16
 * Code subject to the MIT license (https://github.com/hhvm/hsl/blob/master/LICENSE).
 * Copyright (c) 2004-2020, Facebook, Inc. (https://www.facebook.com/)
 *
 * @internal Outside the framework this should not be used directly.
 * @author  Codeigniter
 */
function _from_random(int $length, string $pool): string
{
    if ($length <= 0) {
        throw new InvalidArgumentException(
            sprintf('A strictly positive length is expected, "%d" given.', $length)
        );
    }

    $poolSize = strlen($pool);
    $bits     = (int) ceil(log($poolSize, 2.0));
    if ($bits <= 0 || $bits > 56) {
        throw new InvalidArgumentException(
            'The length of the alphabet must in the [2^1, 2^56] range.'
        );
    }

    $string = '';

    while ($length > 0) {
        $urandomLength = (int) ceil(2 * $length * $bits / 8.0);
        $data          = random_bytes($urandomLength);
        $unpackedData  = 0;
        $unpackedBits  = 0;

        for ($i = 0; $i < $urandomLength && $length > 0; $i++) {
            // Unpack 8 bits
            $unpackedData = ($unpackedData << 8) | ord($data[$i]);
            $unpackedBits += 8;

            // While we have enough bits to select a character from the alphabet, keep
            // consuming the random data
            for (; $unpackedBits >= $bits && $length > 0; $unpackedBits -= $bits) {
                $index = ($unpackedData & ((1 << $bits) - 1));
                $unpackedData >>= $bits;
                // Unfortunately, the alphabet size is not necessarily a power of two.
                // Worst case, it is 2^k + 1, which means we need (k+1) bits and we
                // have around a 50% chance of missing as k gets larger
                if ($index < $poolSize) {
                    $string .= $pool[$index];
                    $length--;
                }
            }
        }
    }

    return $string;
}
/**
 * 根据你指定的类型和长度生成随机字符串。(用于创建密码或生成随机哈希值非常有用。)
 * <br/>注意：对于类型:basic、md5 和 sha1,生成的字符串在加密上不是安全的
 *
 * @param string $type 随机化类型.  basic, alpha, alnum, numeric, nozero, md5, sha1, and crypto:
 * <br/>alpha:仅包含小写和大写字母的字符串。
 * <br/>alnum:包含小写和大写字符的字母数字字符串。
 * <br/>basic:基于 mt_rand() 的随机数(忽略长度)。
 * <br/>numeric:数字字符串。
 * <br/>nozero:不包含零的数字字符串。
 * <br/>md5:基于 md5() 的加密随机数(固定长度 32)。
 * <br/>sha1:基于 sha1() 的加密随机数(固定长度 40)。
 * <br/>crypto:基于 random_bytes() 的随机字符串。$len参数必须为偶数
 * @param int    $len  输出字符串长度
 * @author  Codeigniter
 *
 * @deprecated The type 'basic', 'md5', and 'sha1' are deprecated. They are not cryptographically secure.
 */
function random_string(string $type = 'alnum', int $len = 8): string
{
    switch ($type) {
        case 'alnum':
        case 'nozero':
        case 'alpha':
            $pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
            switch ($type) {
                case 'alpha':
                    $pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
                    break;

                case 'nozero':
                    $pool = '123456789';
                    break;

                case 'alnum':
                default:
                    break;
            }

            return _from_random($len, $pool);

        case 'numeric':
            $max  = 10 ** $len - 1;
            $rand = random_int(0, $max);

            return sprintf('%0' . $len . 'd', $rand);

        case 'md5':
            return md5(uniqid((string) mt_rand(), true));

        case 'sha1':
            return sha1(uniqid((string) mt_rand(), true));

        case 'crypto':
            if ($len % 2 !== 0) {
                throw new InvalidArgumentException(
                    'You must set an even number to the second parameter when you use `crypto`.'
                );
            }

            return bin2hex(random_bytes($len / 2));
    }

    // 'basic' type treated as default
    return (string) mt_rand();
}
/**
 * 通过追加一个数字或增加数字来增加字符串。<br/>
 * 用于创建文件或数据库内容的“副本”非常有用,这些内容具有唯一的标题或 slug。
 *
 * @param string $str       目标字符串，Required
 * @param string $separator 要与重复数字一起追加的分隔符
 * @param int    $first     起始数字
 * @uses 示例：<br/>
 * increment_string('file', '_'); # "file_1"<br/>
 * increment_string('file', '-', 2); # "file-2"<br/>
 * increment_string('file_4'); # "file_5"
 * @author  Codeigniter
 */
function increment_string(string $str, string $separator = '_', int $first = 1): string
{
    preg_match('/(.+)' . preg_quote($separator, '/') . '([0-9]+)$/', $str, $match);

    return isset($match[2]) ? $match[1] . $separator . ((int) $match[2] + 1) : $str . $separator . $first;
}
/**
 * 在循环中允许在两个或多个项目之间交替<br/>
 * 你可以添加尽可能多的参数,并且在每个循环迭代中将返回下一个项目。<br/>
 * 要使用对此函数的多个单独调用,只需不带参数调用该函数即可重新初始化。
 *
 * @param string ...$args 可变数量的参数
 * @return string 交替切换的字符串
 * @author  Codeigniter
 */
function alternator(...$args): string
{
    static $i;

    if (func_num_args() === 0) {
        $i = 0;

        return '';
    }

    return $args[($i++ % count($args))];
}

/**
 * 将字符串中紧邻出现的多个斜杠转换为单个斜杠,但不包括那些在 URL 协议前缀中找到的斜杠(例如 http://)。
 * @param string $str 输入字符串
 * @return string 正规化斜杠的字符串
 * @author  Codeigniter
 */
function reduce_double_slashes(string $str): string
{
    return preg_replace('#(^|[^:])//+#', '\\1/', $str);
}

/**
 * 关键词屏蔽
 * <br>使你能够审查文本字符串中的词。第一个参数将包含原始字符串。第二个将包含你不允许的词数组。第三个(可选)参数可以包含标记词的替换值。如果未指定,它们将被 #### 替换。
 *
 * @param string $str         输入字符串
 * @param array  $censored    要审查的标记词列表
 * @param string $replacement 用什么替换标记词
 * @author  Codeigniter
 */
function word_censor(string $str, array $censored, string $replacement = ''): string
{
    if ($censored === []) {
        return $str;
    }

    $str = ' ' . $str . ' ';

    // \w, \b and a few others do not match on a unicode character
    // set for performance reasons. As a result words like über
    // will not match on a word boundary. Instead, we'll assume that
    // a bad word will be bookended by any of these characters.
    $delim = '[-_\'\"`(){}<>\[\]|!?@#%&,.:;^~*+=\/ 0-9\n\r\t]';

    foreach ($censored as $badword) {
        $badword = str_replace('\*', '\w*?', preg_quote($badword, '/'));

        if ($replacement !== '') {
            $str = preg_replace(
                "/({$delim})(" . $badword . ")({$delim})/i",
                "\\1{$replacement}\\3",
                $str
            );
        } elseif (preg_match_all("/{$delim}(" . $badword . "){$delim}/i", $str, $matches, PREG_PATTERN_ORDER | PREG_OFFSET_CAPTURE)) {
            $matches = $matches[1];

            for ($i = count($matches) - 1; $i >= 0; $i--) {
                $length = strlen($matches[$i][0]);

                $str = substr_replace(
                    $str,
                    str_repeat('#', $length),
                    $matches[$i][1],
                    $length
                );
            }
        }
    }

    return trim($str);
}

/**
 * 使用 HTML 对代码字符串(PHP、HTML等)上色<br/>
 * 此函数使用 PHP 的 highlight_string() 函数,因此使用的颜色是在 php.ini 文件中指定的颜色。
 *
 * Colorizes code strings
 *
 * @param string $str 输入字符串
 * @return string 通过 HTML 着色的字符串
 * @author  Codeigniter
 */
function highlight_code(string $str): string
{
    /* The highlight string function encodes and highlights
     * brackets so we need them to start raw.
     *
     * Also replace any existing PHP tags to temporary markers
     * so they don't accidentally break the string out of PHP,
     * and thus, thwart the highlighting.
     */
    $str = str_replace(
        ['&lt;', '&gt;', '<?', '?>', '<%', '%>', '\\', '</script>'],
        ['<', '>', 'phptagopen', 'phptagclose', 'asptagopen', 'asptagclose', 'backslashtmp', 'scriptclose'],
        $str
    );

    // The highlight_string function requires that the text be surrounded
    // by PHP tags, which we will remove later
    $str = highlight_string('<?php ' . $str . ' ?>', true);

    // Remove our artificially added PHP, and the syntax highlighting that came with it
    $str = preg_replace(
        [
            '/<span style="color: #([A-Z0-9]+)">&lt;\?php(&nbsp;| )/i',
            '/(<span style="color: #[A-Z0-9]+">.*?)\?&gt;<\/span>\n<\/span>\n<\/code>/is',
            '/<span style="color: #[A-Z0-9]+"><\/span>/i',
        ],
        [
            '<span style="color: #$1">',
            "$1</span>\n</span>\n</code>",
            '',
        ],
        $str
    );

    // Replace our markers back to PHP tags.
    return str_replace(
        [
            'phptagopen',
            'phptagclose',
            'asptagopen',
            'asptagclose',
            'backslashtmp',
            'scriptclose',
        ],
        [
            '&lt;?',
            '?&gt;',
            '&lt;%',
            '%&gt;',
            '\\',
            '&lt;/script&gt;',
        ],
        $str
    );
}
/**
 * 将文本字符串中的短语突出显示。
 *
 *
 * @param string $str      原始字符串
 * @param string $phrase   要突出显示的短语
 * @param string $tagOpen  用于突出显示的开标签
 * @param string $tagClose 突出显示的闭标签
 * @return string 通过 HTML 突出显示短语的字符串
 * @remark 旧版浏览器可能不支持新的 HTML5 mark 标签,因此如果你需要支持这样的浏览器, 建议你将以下 CSS 代码插入样式表中:mark {background: #ff0;color: #000;};
 * @author  Codeigniter
 */
function highlight_phrase(string $str, string $phrase, string $tagOpen = '<mark>', string $tagClose = '</mark>'): string
{
    return ($str !== '' && $phrase !== '') ? str_ireplace($phrase, $tagOpen . '\\1' . $tagClose, $str) : $str;
}

/**
 * 从字符串中剥离标签,在定义的最大长度处对其进行拆分,并在前后插入省略号。
 *
 *
 * @param string    $str       要提取摘录的字符串
 * @param int       $maxLength 输出字符串中的字符数
 * @param float|int $position  拆分位置(整数或浮点数)，省略号应出现的字符串中的位置,从 0 到 1,从左到右。例如。值为 1 将省略号放在字符串的右边,.5 在中间,0 在左边。
 * @param string    $ellipsis  要用作省略号的字符 ;默认情况下,将插入'...'
 * @return string 省略处理的字符串
 * @uses 示例：<br/>
 * $str = 'this_string_is_entirely_too_long_and_might_break_my_design.jpg';<br/>
 * echo ellipsize($str, 32, .5);<br/>
 * 输出：this_string_is_e&hellip;ak_my_design.jpg
 * @author  Codeigniter
 */
function ellipsize(string $str, int $maxLength, $position = 1, string $ellipsis = '&hellip;'): string
{
    // Strip tags
    $str = trim(strip_tags($str));

    // Is the string long enough to ellipsize?
    if (mb_strlen($str) <= $maxLength) {
        return $str;
    }

    $beg      = mb_substr($str, 0, (int) floor($maxLength * $position));
    $position = ($position > 1) ? 1 : $position;

    if ($position === 1) {
        $end = mb_substr($str, 0, -($maxLength - mb_strlen($beg)));
    } else {
        $end = mb_substr($str, -($maxLength - mb_strlen($beg)));
    }

    return $beg . $ellipsis . $end;
}
/**
 * 在中心短语 $phrase 前后提取 $radius 个字符,在前后都有省略号。<br/>
 * 注意：当前仅支持拉丁语系
 *
 * @param string $text     要提取摘录的文本
 * @param string $phrase   中心短语:要围绕其提取文本的短语或单词
 * @param int    $radius   $phrase 前后的字符数
 * @param string $ellipsis 要用作省略号的字符
 * @uses 示例：<br/>
 * $text = 'Ut vel faucibus odio. Quisque quis congue libero. Etiam gravida eros lorem, eget porttitor augue dignissim tincidunt. In eget risus eget mauris faucibus molestie vitae ultricies odio. Vestibulum id ultricies diam.Curabitur non mauris lectus. Phasellus eu sodales sem. Integer dictum purus ac enim hendrerit gravida. Donec ac magna vel nunc tincidunt molestie sed vitae nisl. Cras sed auctor mauris, non dictum tortor. Nulla vel scelerisque arcu. Cras ac ipsum sit amet augue laoreet laoreet. Aenean a risus lacus.Sed ut tortor diam.';<br/>
 * echo excerpt($text, 'Donec');<br/>
 * 输出：... non mauris lectus. Phasellus eu sodales sem. Integer dictum purus ac enim hendrerit gravida. Donec ac magna vel nunc tincidunt molestie sed vitae nisl. Cras sed auctor mauris, non dictum tortor. ...
 * @author  Codeigniter
 */
function excerpt(string $text, ?string $phrase = null, int $radius = 100, string $ellipsis = '...'): string
{
    if (isset($phrase)) {
        $phrasePos = stripos($text, $phrase);
        $phraseLen = strlen($phrase);
    } else {
        $phrasePos = $radius / 2;
        $phraseLen = 1;
    }

    $pre = explode(' ', substr($text, 0, $phrasePos));
    $pos = explode(' ', substr($text, $phrasePos + $phraseLen));

    $prev  = ' ';
    $post  = ' ';
    $count = 0;

    foreach (array_reverse($pre) as $e) {
        if ((strlen($e) + $count + 1) < $radius) {
            $prev = ' ' . $e . $prev;
        }
        $count = ++$count + strlen($e);
    }

    $count = 0;

    foreach ($pos as $s) {
        if ((strlen($s) + $count + 1) < $radius) {
            $post .= $s . ' ';
        }
        $count = ++$count + strlen($s);
    }

    $ellPre = $phrase ? $ellipsis : '';

    return str_replace('  ', ' ', $ellPre . $prev . $phrase . $post . $ellipsis);
}

if (! function_exists('is_really_writable')) {
    /**
     * Tests for file writability
     *
     * is_writable() returns TRUE on Windows servers when you really can't write to
     * the file, based on the read-only attribute. is_writable() is also unreliable
     * on Unix servers if safe_mode is on.
     *
     * @see https://bugs.php.net/bug.php?id=54709
     *
     * @throws Exception
     *
     * @codeCoverageIgnore Not practical to test, as travis runs on linux
     * @author  Codeigniter
     */
    function is_really_writable(string $file): bool
    {
        // If we're on a Unix server we call is_writable
        if (! is_windows()) {
            return is_writable($file);
        }

        /* For Windows servers and safe_mode "on" installations we'll actually
         * write a file then read it. Bah...
         */
        if (is_dir($file)) {
            $file = rtrim($file, '/') . '/' . bin2hex(random_bytes(16));
            if (($fp = @fopen($file, 'ab')) === false) {
                return false;
            }

            fclose($fp);
            @chmod($file, 0777);
            @unlink($file);

            return true;
        }

        if (! is_file($file) || ($fp = @fopen($file, 'ab')) === false) {
            return false;
        }

        fclose($fp);

        return true;
    }
}
/**
 * 列出指定目录的文件树
 *
 *
 * @param string $sourceDir      源目录路径，路径几乎总是相对于你的主 index.php 文件。
 * @param int    $directoryDepth 遍历的目录深度(0 = 完全递归, 1 = 当前目录,等等)，深度为 1 只会映射顶级目录
 * @param bool   $hidden         是否包含隐藏路径，默认情况下,返回的数组中不包括隐藏文件,跳过隐藏目录。
 * @return array 文件树：文件数组，每个文件夹名称将是一个数组索引,其包含的文件将以数字索引。如果未找到结果,它将返回一个空数组。
 * @link https://codeigniter.org.cn/user_guide/helpers/filesystem_helper.html#directory_map
 * @author  Codeigniter
 */
function directory_map(string $sourceDir, int $directoryDepth = 0, bool $hidden = false): array
{
    try {
        $fp = opendir($sourceDir);

        $fileData  = [];
        $newDepth  = $directoryDepth - 1;
        $sourceDir = rtrim($sourceDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;

        while (false !== ($file = readdir($fp))) {
            // Remove '.', '..', and hidden files [optional]
            if ($file === '.' || $file === '..' || ($hidden === false && $file[0] === '.')) {
                continue;
            }

            if (is_dir($sourceDir . $file)) {
                $file .= DIRECTORY_SEPARATOR;
            }

            if (($directoryDepth < 1 || $newDepth > 0) && is_dir($sourceDir . $file)) {
                $fileData[$file] = directory_map($sourceDir . $file, $newDepth, $hidden);
            } else {
                $fileData[] = $file;
            }
        }

        closedir($fp);

        return $fileData;
    } catch (Throwable) {
        return [];
    }
}

/**
 * 获取指定服务器路径的所有文件名的数组。(使用RecursiveDirectoryIterator)
 *
 * @param string    $sourceDir 目录路径
 * @param bool|null $includePath 是否将路径作为文件名的一部分包含;false 不包含路径,null 包含相对于 $sourceDir 的路径(即：获取相对路径),true 包含完整路径,
 * @param bool      $hidden      是否包含隐藏文件(以句点开头的文件)
 * @param bool      $includeDir  是否在数组输出中包含目录
 * @param bool      $test_separator  是否检测目录分隔符
 *
 * @link https://codeigniter.org.cn/user_guide/helpers/filesystem_helper.html#get_filenames
 * @author  Codeigniter
 */
function get_filenames(string $sourceDir,?bool $includePath = false,bool $hidden = false,bool $includeDir = true,bool $test_separator = true): array
{
    $files = [];

    if($test_separator){
        if(DIRECTORY_SEPARATOR == '/') $sourceDir = str_replace('\\',DIRECTORY_SEPARATOR,$sourceDir);
        else $sourceDir = str_replace('/',DIRECTORY_SEPARATOR,$sourceDir);
    }

    $sourceDir = realpath($sourceDir) ?: $sourceDir;
    $sourceDir = rtrim($sourceDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;

    try {
        foreach (new RecursiveIteratorIterator(
                     new RecursiveDirectoryIterator($sourceDir, RecursiveDirectoryIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS),
                     RecursiveIteratorIterator::SELF_FIRST
                 ) as $name => $object) {
            $basename = pathinfo($name, PATHINFO_BASENAME);
            if (! $hidden && $basename[0] === '.') {
                continue;
            }

            if ($includeDir || ! $object->isDir()) {
                if ($includePath === false) {
                    $files[] = $basename;
                } elseif ($includePath === null) {
                    $files[] = str_replace($sourceDir, '', $name);
                } else {
                    $files[] = $name;
                }
            }
        }
    } catch (Throwable) {
        return [];
    }

    sort($files);

    return $files;
}

/**
 * 根据文件和路径,返回(可选地) 名称、路径、大小 和 修改日期 信息属性。
 *
 * @param string $file 文件路径
 * @param array|string $returnedValues 要作为数组或逗号分隔字符串返回的信息类型
 *
 * @return array|null 包含指定文件的信息的数组,失败为 false
 * @link https://codeigniter.org.cn/user_guide/helpers/filesystem_helper.html#get_file_info
 * @author  Codeigniter
 */
function get_file_info(string $file, $returnedValues = ['name', 'server_path', 'size', 'date'])
{
    if (! is_file($file)) {
        return null;
    }

    $fileInfo = [];

    if (is_string($returnedValues)) {
        $returnedValues = explode(',', $returnedValues);
    }

    foreach ($returnedValues as $key) {
        switch ($key) {
            case 'name':
                $fileInfo['name'] = basename($file);
                break;

            case 'server_path':
                $fileInfo['server_path'] = $file;
                break;

            case 'size':
                $fileInfo['size'] = filesize($file);
                break;

            case 'date':
                $fileInfo['date'] = filemtime($file);
                break;

            case 'readable':
                $fileInfo['readable'] = is_readable($file);
                break;

            case 'writable':
                $fileInfo['writable'] = is_really_writable($file);
                break;

            case 'executable':
                $fileInfo['executable'] = is_executable($file);
                break;

            case 'fileperms':
                $fileInfo['fileperms'] = fileperms($file);
                break;
        }
    }

    return $fileInfo;
}
/**
 * 写文件：将数据写入路径中指定的文件。如果文件不存在,则该函数将创建它。
 *
 * @param string $path 文件路径
 * @param string $data 要写入文件的数据
 * @param string $mode fopen() 模式 (default: 'wb')
 *
 * @return bool 如果写入成功则为 true,如果有错误则为 false
 * @link https://codeigniter.org.cn/user_guide/helpers/filesystem_helper.html#write_file
 * @link https://www.php.net/manual/en/function.fopen.php
 * @author  Codeigniter
 */
function write_file(string $path, string $data, string $mode = 'wb'): bool
{
    try {
        $fp = fopen($path, $mode);

        flock($fp, LOCK_EX);

        for ($result = $written = 0, $length = strlen($data); $written < $length; $written += $result) {
            if (($result = fwrite($fp, substr($data, $written))) === false) {
                break;
            }
        }

        flock($fp, LOCK_UN);
        fclose($fp);

        return is_int($result);
    } catch (Throwable) {
        return false;
    }
}

/**
 * Force Download
 *
 * Generates headers that force a download to happen
 *生成 HTTP 头强制下载数据到客户端，这在实现文件下载时很有用。 第一个参数为下载文件名称，第二个参数为文件数据。
 *如果第二个参数为空，并且 $filename 参数是一个存在并可读的文件路径， 那么这个文件的内容将被下载。
 *如果第三个参数设置为 TRUE，那么将发送文件实际的 MIME 类型（根据文件的扩展名）， 这样你的浏览器会根据该 MIME 类型来处理。
 *这函数放到台湾那边的服务器上面，一直出问题，
 * 下载档案名乱码的通过mb_convert_encoding($detail['f1_name'],"BIG-5","UTF-8");解决了。
 * 但是下载的文件总是0kb，就是下载失败啊。
 * @param string    filename
 * @param mixed    the data to be downloaded
 * @param bool    whether to try and send the actual file MIME type
 * @return    void
 */
function force_download($filename = '', $data = '', $set_mime = FALSE)
{
    if ($filename === '' OR $data === '') {
        return;
    } elseif ($data === NULL) {
        if (!@is_file($filename) OR ($filesize = @filesize($filename)) === FALSE) {
            return;
        }

        $filepath = $filename;
        $filename = explode('/', str_replace(DIRECTORY_SEPARATOR, '/', $filename));
        $filename = end($filename);
    } else {
        $filesize = strlen($data);
    }

    // Set the default MIME type to send
    $mime = 'application/octet-stream';

    $x = explode('.', $filename);
    $extension = end($x);

    if ($set_mime === TRUE) {
        if (count($x) === 1 OR $extension === '') {
            /* If we're going to detect the MIME type,
             * we'll need a file extension.
             */
            return;
        }

        // Load the mime types
        $mimes = &Mimes::$mimes;

        // Only change the default MIME if we can find one
        if (isset($mimes[$extension])) {
            $mime = is_array($mimes[$extension]) ? $mimes[$extension][0] : $mimes[$extension];
        }
        unset($mimes);
    }

    /* It was reported that browsers on Android 2.1 (and possibly older as well)
     * need to have the filename extension upper-cased in order to be able to
     * download it.
     *
     * Reference: http://digiblog.de/2011/04/19/android-and-the-download-file-headers/
     */
    if (count($x) !== 1 && isset($_SERVER['HTTP_USER_AGENT']) && preg_match('/Android\s(1|2\.[01])/', $_SERVER['HTTP_USER_AGENT'])) {
        $x[count($x) - 1] = strtoupper($extension);
        $filename = implode('.', $x);
    }

    if ($data === NULL && ($fp = @fopen($filepath, 'rb')) === FALSE) {
        return;
    }

    // Clean output buffer
    if (ob_get_level() !== 0 && @ob_end_clean() === FALSE) {
        @ob_clean();
    }

    // Generate the server headers
    header('Content-Type: ' . $mime);
    header('Content-Disposition: attachment; filename="' . $filename . '"');
    header('Expires: 0');
    header('Content-Transfer-Encoding: binary');
    header('Content-Length: ' . $filesize);
    header('Cache-Control: private, no-transform, no-store, must-revalidate');

    // If we have raw data - just dump it
    if ($data !== NULL) {
        exit($data);
    }

    // Flush 1MB chunks of data
    while (!feof($fp) && ($data = fread($fp, 1048576)) !== FALSE) {
        echo $data;
    }

    fclose($fp);
    exit;
}

/**
 * 获取数字权限(例如 fileperms() 返回的)并返回标准符号表示法的文件权限。
 *
 * @param int $perms 权限
 * @return string 符号权限字符串
 * @link https://codeigniter.org.cn/user_guide/helpers/filesystem_helper.html#symbolic_permissions
 * @uses 示例：<br/>
 * echo symbolic_permissions(fileperms('./index.php'));  //输出：-rw-r--r--
 * @author  Codeigniter
 */
function symbolic_permissions(int $perms): string
{
    if (($perms & 0xC000) === 0xC000) {
        $symbolic = 's'; // Socket
    } elseif (($perms & 0xA000) === 0xA000) {
        $symbolic = 'l'; // Symbolic Link
    } elseif (($perms & 0x8000) === 0x8000) {
        $symbolic = '-'; // Regular
    } elseif (($perms & 0x6000) === 0x6000) {
        $symbolic = 'b'; // Block special
    } elseif (($perms & 0x4000) === 0x4000) {
        $symbolic = 'd'; // Directory
    } elseif (($perms & 0x2000) === 0x2000) {
        $symbolic = 'c'; // Character special
    } elseif (($perms & 0x1000) === 0x1000) {
        $symbolic = 'p'; // FIFO pipe
    } else {
        $symbolic = 'u'; // Unknown
    }

    // Owner
    $symbolic .= ((($perms & 0x0100) !== 0) ? 'r' : '-')
        . ((($perms & 0x0080) !== 0) ? 'w' : '-')
        . ((($perms & 0x0040) !== 0) ? ((($perms & 0x0800) !== 0) ? 's' : 'x') : ((($perms & 0x0800) !== 0) ? 'S' : '-'));

    // Group
    $symbolic .= ((($perms & 0x0020) !== 0) ? 'r' : '-')
        . ((($perms & 0x0010) !== 0) ? 'w' : '-')
        . ((($perms & 0x0008) !== 0) ? ((($perms & 0x0400) !== 0) ? 's' : 'x') : ((($perms & 0x0400) !== 0) ? 'S' : '-'));

    // World
    $symbolic .= ((($perms & 0x0004) !== 0) ? 'r' : '-')
        . ((($perms & 0x0002) !== 0) ? 'w' : '-')
        . ((($perms & 0x0001) !== 0) ? ((($perms & 0x0200) !== 0) ? 't' : 'x') : ((($perms & 0x0200) !== 0) ? 'T' : '-'));

    return $symbolic;
}

/**
 * 获取数字权限(例如 fileperms() 返回的)并返回三字符八进制表示法的文件权限。
 *
 * @param int $perms 权限
 * @return string 八进制权限字符串
 * @uses 示例：<br/>
 * echo octal_permissions(fileperms('./index.php')); //输出：644
 * @author  Codeigniter
 */
function octal_permissions(int $perms): string
{
    return substr(sprintf('%o', $perms), -3);
}

/**
 * 从HTML代码中取出一个属性值(但不限于HTML)
 * @param string &$html HTML代码
 * @param string $property_name 属性名称
 * @param string $before_exp 前导正则式,不能包含界定符和说明符,默认为空
 * @param string $after_exp 后缀正则式,不能包含界定符和说明符,默认为空
 * @return false|string
 */
function FindHtmlTagProperty(string &$html, string $property_name,string $before_exp='',string $after_exp='')
{
    if (empty($html) || empty($property_name)) return false;
    $pn_len = strlen($property_name);
    $html_len = strlen($html);
    if ($pn_len >= $html_len) return false;
    $strarr = preg_split('//us', $html, -1, PREG_SPLIT_NO_EMPTY);
    $html_len = count($strarr);
    $subchr = '*';//不能设置为空白字符,影响下一步的匹配

    $repls = array();
    $quotchr = 0;
    $backslash = 0;
    for ($i = 0; $i < $html_len; $i++) {
        $chri = ord($strarr[$i]);
        if ($chri == 34 || $chri == 39) {
            if ($quotchr == 0) {
                $quotchr = $chri;
            } elseif ($quotchr == $chri) {
                if ($backslash % 2 == 1) {
                    $repls[] = array($i, $strarr[$i]);
                    $strarr[$i] = $subchr;
                } else
                    $quotchr = 0;
            }
            $backslash = 0;
        } else {
            if ($chri == 92) {
                $backslash++;
            } else {
                $backslash = 0;
            }
        }
    }
    $newStr = null;
    if (count($repls) > 0) $newStr = implode('', $strarr);
    else $newStr = $html;
    $matches = null;
    if (preg_match('/.*?'.$before_exp.'\b' . preg_quote($property_name) . '\s*=\s*((?P<quot>["\'])(?P<prop>.*?)\k<quot>|(?P<prop2>[^\s]+))'.$after_exp.'/ius', $newStr, $matches, PREG_OFFSET_CAPTURE) > 0) {
        $rc = count($repls);
        if ($rc == 0) {
            if ($matches['prop'][1] > -1) {
                return $matches['prop'][0];
            }
            return $matches['prop2'][0];
        } else {
            if ($matches['prop'][1] > -1) {
                return substr($html,$matches['prop'][1],strlen($matches['prop'][0]));
            }
            return substr($html,$matches['prop2'][1],strlen($matches['prop2'][0]));
        }
    }
    return false;
}

/**
 * 获取调用的插件名字
 * @param string|null $path
 * @return bool|false|mixed|string
 */
function get_plugin_name($path = null)
{
    /**
     * 解析路径中的插件名字,仅用于内部
     * @param string $path
     * @return bool|false|string
     */
    function _ResolutPluginPath($path)
    {
        $pluginDir = APP_PATH . 'plugin' . DIRECTORY_SEPARATOR;
        if ('/' != DIRECTORY_SEPARATOR) $pluginDir = str_replace('/', DIRECTORY_SEPARATOR, $pluginDir);
        $pluginDirLen = strlen($pluginDir);
        $callfile = $path;
        $ret = false;
        if (stripos($callfile, $pluginDir) === 0) {
            $dirsp = strpos($callfile, DIRECTORY_SEPARATOR, $pluginDirLen);
            if ($dirsp > 0) {
                $ret = substr($callfile, $pluginDirLen, $dirsp - $pluginDirLen);
            }
        } else {
            $tmpFileName = basename($callfile);
            $startpos = -1;
            $startwords = array('plugin_', 'admin_plugin_');
            $endwords = array('_model_', '_route_');
            foreach ($startwords as $startword) {
                if (stripos($tmpFileName, $startword) === 0) {
                    $startpos = strlen($startword);
                    $find = false;
                    foreach ($endwords as $endword) {
                        $endpos = stripos($tmpFileName, $endword, $startpos);
                        if ($endpos > 0) {
                            $find = true;
                            $ret = substr($tmpFileName, $startpos, $endpos - $startpos);
                            break;
                        }
                    }
                    if ($find) break;
                }
            }
        }
        return $ret;
    }

    if (!empty($path)) {
        return _ResolutPluginPath($path);
    }
    if (function_exists('debug_backtrace')) {
        $traces = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
        if (!empty($traces)) {
            $callplugins = array();
            foreach ($traces as $trace) {
                $tryName = _ResolutPluginPath($trace['file']);
                if ($tryName != false)
                    $callplugins[] = $tryName;
            }
            if (!empty($callplugins))
                return $callplugins[count($callplugins) - 1];//返回最上层的插件名
        }
    }
    return _ResolutPluginPath(__FILE__);
}

/**
 * 读取插件的配置(返回引用时,慎重修改)
 * @param string $dir 插件名称或者配置的目录
 * @param boolean $fromfile 是否从文件读取,默认为false
 * @return bool|array|null
 */
function &get_plugin_config($dir, $fromfile = false)
{
    static $plugins_cfg = array();
    static $apppath_std = null;
    if(empty($apppath_std)) $apppath_std = (DIRECTORY_SEPARATOR == '/' ? str_replace('\\', DIRECTORY_SEPARATOR, APP_PATH) : str_replace('/', DIRECTORY_SEPARATOR, APP_PATH));
    $ret = null;
    if (!empty($dir)) {
        $full_path = null;
        $pluginName = null;
        if (strpos($dir, '\\') === false && strpos($dir, '/') === false) {
            $pluginName = $dir;
            $dir = $apppath_std . 'plugin' . DIRECTORY_SEPARATOR . $dir . DIRECTORY_SEPARATOR;
        } else {
            if(!is_file($dir)){
                $dirlen = strlen($dir);
                if ($dir[$dirlen - 1] == '\\' || $dir[$dirlen - 1] == '/')
                    $dir = substr($dir, 0, $dirlen - 1);
                $pluginName = basename($dir);
            }
            else{
                $dir = dirname($dir);
            }
            $dir .= DIRECTORY_SEPARATOR;
        }
        $full_path = $dir . 'conf.json';
        if (!$fromfile) {
            if(isset($plugins_cfg[$pluginName]) && !empty($plugins_cfg[$pluginName]))
                $ret = &$plugins_cfg[$pluginName];
            else
                $fromfile = true;
        }
        if($fromfile || empty($ret)){
            if (is_file($full_path)) {
                $ret = xn_json_decode(file_get_contents($full_path));
                if (!empty($ret)) {
                    //自动初始化:
                    if (isset($ret['ConfigAutoInitialization'])) {
                        $aiopt = $ret['ConfigAutoInitialization'];
                        if(is_string($aiopt) && is_callable($aiopt)){//是有效的函数
                            call_user_func_array($aiopt,array($ret,$pluginName));
                        }
                    }
                    $plugins_cfg[$pluginName] = &$ret;
                }
            }
        }
    }
    return $ret;
}

/**
 * 将配置数组写入conf.json,注意:会更新配置缓存
 * @param array &$data 配置数组
 * @param string $dir 配置文件的目录路径
 * @param string $errorMsg 错误信息,需要读取时传入引线
 * @return array|int 正确执行返回新的配置數組,负数为错误信息,-1:参数空;-2:目录不存在;-3:文件不可写;-4:内容写入错误
 */
function put_plugin_config(array &$data, $dir, string &$errorMsg = null)
{
    if (empty($data) || empty($dir)) {
        $errorMsg = '保存的配置数据或者路径为空';
        return -1;
    }
    if(isset($data['ConfigSerializationPreFun']) && is_callable($data['ConfigSerializationPreFun'])){
       $dataX = call_user_func($data['ConfigSerializationPreFun'],$data);
        unset($data);
        $data = &$dataX;//不要修改原配置,以免再次手工初始化
    }
    $pluginName = null;
    if (strpos($dir, '\\') === false && strpos($dir, '/') === false) {
        $pluginName = $dir;
        $dir = APP_PATH . 'plugin' . DIRECTORY_SEPARATOR . $dir . DIRECTORY_SEPARATOR;
    } else {
        $dirlen = strlen($dir);
        if ($dir[$dirlen - 1] == '\\' || $dir[$dirlen - 1] == '/')
            $dir = substr($dir, 0, $dirlen - 1);
        $pluginName = basename($dir);
        $dir .= DIRECTORY_SEPARATOR;
    }
    $conffile = $dir . 'conf.json';
    if (!is_dir(dirname($conffile))) {//|| !xn_is_writable($conffile))
        $errorMsg = '保存的文件路径不存在';
        return -2;//不允许另外创建不存在的目录
    } elseif (!xn_is_writable($conffile)) {
        $str = xn_json_encode($data, true);
        $errorMsg = '保存的文件路径不可写,请将如下信息手动写入到文件(' . $conffile . '):' . PHP_EOL . PHP_EOL . $str;
        return -3;
    }
    $ret = file_replace_var($conffile, $data, true);
    $enabled_plugins = plugin_paths_enabled();
    if ((isset($ret['installed']) && empty($ret['installed'])) || (isset($ret['enable']) && empty($ret['enable']))) {
        if (isset($enabled_plugins[$pluginName]))//從緩存中刪除
        {
            unset($enabled_plugins[$pluginName]);
            plugin_clear_tmp_dir();//清除缓存重新生成
        }
    } else
        $enabled_plugins[$pluginName] = $ret;//更新配置
    return $ret;
}





?>